feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256
feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256rpatel-scale wants to merge 1 commit into
Conversation
Create the agent registry row at build time instead of only at deploy
time, so an agent resource (and id) exists before deployment and can be
permissioned/shared up front.
- Add `BUILD_ONLY` ("BuildOnly") value to AgentStatus (entity + schema)
and an alembic migration adding it to the Postgres `agentstatus` enum.
- Add `POST /agents/register-build`: creates the agent row without an
acp_url, in BUILD_ONLY status, and grants the caller access. Unlike
/register it does not mint an API key. Idempotent by name so a rebuild
never clobbers a live deployment.
- Deploy-time /register continues to flip the agent to READY and set the
acp_url, unchanged.
- Regenerate openapi.yaml for the new endpoint.
Part of the agent-creation/authz work tracked in the Agentex agent
creation Authz doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✱ Stainless preview buildsThis PR will update the openapi python typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ agentex-sdk-openapi studio · code · diff
✅ agentex-sdk-typescript studio · code · diff
✅ agentex-sdk-python studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
| await authorization_service.grant( | ||
| AgentexResource.agent(agent_entity.id), | ||
| principal_context=request.principal_context, | ||
| ) | ||
| return Agent.model_validate(agent_entity) |
There was a problem hiding this comment.
Grant issued unconditionally on idempotent return
authorization_service.grant is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (register_build found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard create permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The /register endpoint has a comparable pattern but requires a live acp_url, giving stronger proof of ownership; register-build has no such constraint, lowering the bar considerably.
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/api/routes/agents.py
Line: 256-260
Comment:
**Grant issued unconditionally on idempotent return**
`authorization_service.grant` is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (`register_build` found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard `create` permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The `/register` endpoint has a comparable pattern but requires a live `acp_url`, giving stronger proof of ownership; `register-build` has no such constraint, lowering the bar considerably.
How can I resolve this? If you propose a fix, please make it concise.
What
Adds the first building block for creating an agent's registry row at build time instead of only at deploy time, so an agent resource (and
id) exists before deployment and can be permissioned/shared up front.BUILD_ONLYagent status ("BuildOnly"), added toAgentStatusin both the domain entity and the API schema, plus an Alembic migration adding the value to the Postgresagentstatusenum (ALTER TYPE ... ADD VALUE IF NOT EXISTS, mirroringadd_unhealthy_status).POST /agents/register-build— creates the agent row without anacp_url(no running pod yet), inBUILD_ONLYstatus, and grants the caller access. Unlike/register, it does not mint an API key. Idempotent by name, so re-building an existing agent never clobbers a live deployment's status/acp_url./registeris unchanged — registering with the agent'sagent_idstill flips it toREADYand sets theacp_url.openapi.yaml.Why
Today the agent registry row is only created at deploy/register time. Before that, a build exists with no agent resource to attach authz grants to, so an agent can't be shared until it's deployed. Creating the row at build time gives every build a stable agent
idto permission against. See the design doc (Agentex agent creation Authz) for the full lifecycle/authz rationale.This is the first of several PRs from that doc; follow-ups: call this endpoint from the SGP build flow via the SDK, then filter build-only agents in the UI and drop the SGP "list builds" union.
Lifecycle
Tests
New integration tests in
tests/integration/api/agents/test_agents_api.py:register-buildcreates aBuildOnlyagent with noacp_urland no API key, retrievable like any agent.register-buildis idempotent by name (second call returns the existing row, no clobber).BUILD_ONLYagent is promoted toReadyby a subsequent/registerwith itsagent_id.Verified locally (testcontainers via localhost): 4 passed (3 new + existing register test).
ruff,ruff-format, and the migration-safety linter all pass.🤖 Generated with Claude Code
Greptile Summary
This PR introduces a
POST /agents/register-buildendpoint that creates an agent row inBUILD_ONLYstatus (noacp_url) at build time, so a stable agentidexists for permissioning before any pod is deployed. A matching Alembic migration adds theBUILD_ONLYvalue to the Postgresagentstatusenum./agents/register-buildcreates an agent inBUILD_ONLYstatus, grants the caller access, mints no API key, and is idempotent by name.BUILD_ONLYstatus added to both the domain entity and API schema enums, with a safeIF NOT EXISTSmigration mirroring the existingadd_unhealthy_statuspattern.READYvia a subsequent/registercall.Confidence Score: 3/5
The core build-registration logic is sound, but the endpoint unconditionally grants the caller access to any agent matching the requested name, including agents they did not create.
The new register-build endpoint is well-structured and the use-case idempotency handling is correct. However, authorization_service.grant fires on every call — including the idempotent path that simply returns an existing agent unchanged. Because the endpoint requires no acp_url and no proof of ownership, any caller with wildcard create permission can obtain a grant on any existing agent by knowing its name. The integration tests exercise idempotency but use the same client for both calls, so cross-principal grant behavior is not covered.
agentex/src/api/routes/agents.py — the register_build route handler needs a guard to skip the grant call when an existing agent is returned.
Security Review
agentex/src/api/routes/agents.py,register_build):authorization_service.grantis called unconditionally even when the use case returns an existing agent (the idempotent path). Any caller with wildcardcreatepermission can gain a grant on any existing agent by callingregister-buildwith the target agent's name, without supplying anacp_urlor making any modification to the agent.Important Files Changed
Sequence Diagram
sequenceDiagram participant Client participant Route as POST /agents/register-build participant AuthSvc as AuthorizationService participant UseCase as AgentsUseCase participant Repo as AgentRepository Client->>Route: "POST /agents/register-build {name, description, ...}" Route->>AuthSvc: "check(agent("*"), create, principal_context)" AuthSvc-->>Route: allowed Route->>UseCase: register_build(name, description, ...) UseCase->>Repo: "get(name=name)" alt Agent already exists Repo-->>UseCase: existing AgentEntity UseCase-->>Route: existing AgentEntity (unchanged) else ItemDoesNotExist UseCase->>Repo: "create(AgentEntity{status=BUILD_ONLY, acp_url=None})" alt DuplicateItemError (race) Repo-->>UseCase: error UseCase->>Repo: "get(name=name)" Repo-->>UseCase: AgentEntity end Repo-->>UseCase: new AgentEntity UseCase-->>Route: new AgentEntity end Route->>AuthSvc: grant(agent(id), principal_context) Note over Route,AuthSvc: grant runs even on existing-agent path AuthSvc-->>Route: granted Route-->>Client: "200 Agent{status=BuildOnly, acp_url=null}" Note over Client,Repo: Later, at deploy time: Client->>+Route: "POST /agents/register {agent_id, acp_url, ...}" Route-->>-Client: "200 Agent{status=Ready, acp_url=set}"Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(agentex): add register-build endpoi..." | Re-trigger Greptile